<?php
/**
 * @author		João Batista Neto
 * @brief		Objetos que trabalham com requisição e retorno da API BuscaPé
 * @package		dso.buscape.result
 */

require_once 'rpo/base/Object.php';
require_once 'dso/buscape/result/BuscapeResultBuilder.php';
require_once 'dso/buscape/result/BuscapeResultDirector.php';

/**
 * @brief		Director para construção do resultado a partir de um XML
 * @class		BuscapeXMLResultDirector
 */
class BuscapeXMLResultDirector extends Object implements BuscapeResultDirector {
	/**
	 * @brief	Builder
	 * @var		BuscapeResultBuilder
	 */
	private $builder;

	/**
	 * @brief	Documento XML
	 * @var		DOMDocument
	 */
	private $dom;

	/**
	 * @var		DOMXpath
	 */
	private $xpath;

	/**
	 * @brief	Constroi o Director responsável pela criação do objeto de retorno
	 */
	public function __construct(){
		$this->builder = new BuscapeResultBuilder();
	}

	/**
	 * @brief	Recupera o resultado
	 * @return	BuscapeResult
	 */
	public function getResult(){
		$this->parseResult();

		return $this->builder->getResult();
	}

	/**
	 * @brief	Recupera o valor de um nó do XML
	 * @param	$context DOMElement O nó de contexto que contém o filho que se deseja
	 * @param	$nodeName string O nome do nó que se deseja o valor
	 * @param	$type string Tipo de dado do retorno
	 * @return	mixed O tipo de dado retornado é o equivalente ao passado pelo parâmetro $type
	 */
	private function getNodeValue( DOMElement $context , $nodeName , $type = 'string' ){
		$node = $context->getElementsByTagName( $nodeName )->item( 0 );

		if ( !is_null( $node ) ){
			$nodeValue = $node->nodeValue;
		} else {
			$nodeValue = null;
		}

		return $this->setType( $nodeValue , $type );
	}

	/**
	 * @brief	Recupera o valor de um atributo
	 * @param	$context DOMElement O nó de contexto que contém o atributo desejado
	 * @param	$attributeName string Nome do atributo desejado
	 * @param	$type string Tipo de dado do retorno
	 * @return	mixed O tipo de dado retornado é o equivalente ao passado pelo parâmetro $type
	 */
	private function getAttributeValue( DOMElement $context = null , $attributeName , $type = 'string' ){
		$attributeValue = null;

		if ( !is_null( $context ) ){
			$attributeValue = $context->getAttribute( $attributeName );
		}

		return $this->setType( $attributeValue , $type );
	}

	/**
	 * @brief	Define o tipo de um valor
	 * @param	$value string O valor original
	 * @param	$type string O tipo de dado do retorno
	 * @return	mixed O tipo de dado retornado é o equivalente ao passado pelo parâmetro $type
	 */
	private function setType( $value , $type = 'string' ){
		switch ( $type ){
			case 'bool':
			case 'boolean':
				$booleanValue = strtolower( $value );

				if ( ( $booleanValue == 'true' ) || ( $booleanValue == 'false' ) ){
					$value = $booleanValue == 'true';
				} else {
					$value = (bool) $booleanValue;
				}

				break;
			case 'float':
				$value = (float) $value;
				break;
			case 'int':
			case 'integer':
				$value = (int) $value;
				break;
			case 'string':
			default:
				$value = (string) $value;

		}

		return $value;
	}

	/**
	 * @brief	Define a resposta HTTP
	 * @param	$response string Resposta da requisição
	 * @return	BuscapeResultDirector Este objeto
	 */
	public function setHTTPResponse( $response ){
//		$dom = new DOMDocument();
//		$dom->loadXML( $response );
//
//		echo $dom->saveXML();

		$this->dom = new DOMDocument();
		$this->dom->preserveWhiteSpace = false;
		$this->dom->loadXML( $response );


		return $this;
	}

	/**
	 * @brief	Interpreta os resultados retornados
	 */
	private function parseResult(){
		$result = $this->dom->getElementsByTagName( 'Result' )->item( 0 );
		$totalResultsReturned = $this->getAttributeValue( $result , 'totalResultsReturned' , 'int' );
		$totalResultsAvailable = $this->getAttributeValue( $result , 'totalResultsAvailable' , 'int' );

		if ( $totalResultsReturned == $totalResultsAvailable ){
			$page = 1;
			$totalPages = 1;
		} else {
			$page = $this->getAttributeValue( $result , 'page' , 'int' );
			$totalPages= $this->getAttributeValue( $result , 'totalPages' , 'int' );
		}

		$this->xpath = new DOMXPath( $this->dom );
		$this->xpath->registerNamespace( 'bp' , 'urn:buscape' );

		$this->builder->buildResult( array(
			'totalResultsReturned'	=> $totalResultsReturned,
			'totalResultsAvailable'	=> $totalResultsAvailable,
			'page'					=> $page,
			'totalPages'			=> $totalPages,
			'match'					=> $this->getAttributeValue( $result , 'match' ),
			'seller'				=> $this->parseSellers( $result )
		) );

		$this->parseResultDetails( $result );
	}

	/**
	 * @brief	Interpreta o nó details do XML
	 * @param	$result DOMElement Nó Result do XML
	 */
	private function parseResultDetails( DOMElement $result ){
		$details = $result->getElementsByTagName( 'details' )->item( 0 );
		$data = array();

		foreach ( $details->childNodes as $childNode ){
			$data[ $childNode->nodeName ] = $childNode->nodeValue;
		}

		$this->builder->buildResultDetails( $data );

		$this->parseCategories( $result );
		$this->parseProducts( $result );
	}

	/**
	 * @brief	Interpreta uma lista de endereços de um vendedor
	 * @param	$seller DOMElement Nó do vendedor
	 * @return	RecursiveComponentIterator
	 */
	private function parseAddresses( DOMElement $seller ){
		$addresses = $this->builder->buildIterator();

		foreach ( $seller->getElementsByTagName( 'address' ) as $address ){
			$name = $this->getNodeValue( $address , 'name' );
			$postalArea = $this->getNodeValue( $address , 'postalArea' );
			$complement = $this->getNodeValue( $address , 'complement' );
			$district = $this->getNodeValue( $address , 'district' );
			$city = $this->getNodeValue( $address , 'city' );
			$state = $this->getNodeValue( $address , 'state' );
			$postalCode = $this->getNodeValue( $address , 'postalCode' );
			$phone = $this->getNodeValue( $address , 'phone' ); //FIXME: Esse nó precisa ser traduzido
			$map = $address->getElementsByTagName( 'map' )->item( 0 );
			$latitude = $this->getNodeValue( $map , 'latitude' , 'float' );
			$longitude = $this->getNodeValue( $map , 'longitude' , 'float' );

			$buscapeAddress = $this->builder->buildAddress( array(
				'name'			=> $name,
				'postalArea'	=> $postalArea,
				'complement'	=> $complement,
				'district'		=> $district,
				'city'			=> $city,
				'state'			=> $state,
				'postalCode'	=> $postalCode,
				'phone'			=> $phone,
				'latitude'		=> $latitude,
				'longitude'		=> $longitude
			) );

			$addresses->append( $buscapeAddress );
		}

		return $addresses;
	}

	/**
	 * @brief	Interpreta as categorias do XML
	 * @param	$result DOMElement Nó Result do XML
	 */
	private function parseCategories( DOMElement $result ){
		$categories = array();

		foreach ( $this->xpath->query( './/bp:category|.//bp:subCategory|.//bp:top5Category' , $result ) as $category ){
			$this->parseCategory( $result , $category , $categories );
		}

		$this->builder->buildCategories( $categories );
	}

	/**
	 * @brief	Interpreta uma categoria do XML
	 * @param	$result DOMElement Nó Result do XML
	 * @param	$category DOMElement Nó da categoria que será interpretada
	 * @param	$categories array Lista de categorias
	 */
	private function parseCategory( DOMElement $result , DOMElement $category , array &$categories ){
		$id = (int) $category->getAttribute( 'id' );

		if ( !isset( $categories[ $id ] ) ){
			$hasOffer = $this->getAttributeValue( $category , 'hasOffer' , 'bool' );
			$isFinal = $this->getAttributeValue( $category , 'isFinal' , 'bool' );
			$parentCategoryId = $this->getAttributeValue( $category , 'parentCategoryId' , 'int' );
			$name = $this->getNodeValue( $category , 'name' );
			$top5Category = $category->localName == 'top5Category';
			$buscapeCategory = BuscapeCategory::createInstante( array(
				'hasOffer'		=> $hasOffer,
				'id'			=> $id,
				'isFinal'		=> $isFinal,
				'name'			=> $name,
				'thumbnail'		=> $this->builder->buildThumbnail( array(
					'url' => $this->getAttributeValue( $category->getElementsByTagName( 'thumbnail' )->item( 0 ) , 'url' )
				) ),
				'top5Category'	=> $top5Category,
				'links'			=> $this->parseLinks( $category )
			) );

			if ( $id !== $parentCategoryId ){
				if ( !isset( $categories[ $parentCategoryId ] ) ){
					$query = sprintf( './/bp:category[@id="%d"]|.//bp:subCategory[@id="%d"]|.//bp:top5Category[@id="%d"]' , $parentCategoryId , $parentCategoryId , $parentCategoryId );
					$super = $this->xpath->query( $query )->item( 0 );

					if ( !is_null( $super ) ){
						$this->parseCategory( $result , $super , $categories );
					}
				}

				if ( isset( $categories[ $parentCategoryId ] ) ){
					$categories[ $parentCategoryId ][ 0 ]->add( $buscapeCategory );
					$categories[ $id ] = array( $buscapeCategory , false );
				} else {
					$categories[ $id ] = array( $buscapeCategory , true );
				}
			} else {
				$categories[ $id ] = array( $buscapeCategory , true );
			}
		}
	}

	/**
	 * @brief	Interpreta uma lista de links
	 * @param	$context DOMElement Nó de contexto
	 * @return	RecursiveComponentIterator Lista com os links
	 */
	private function parseLinks( DOMElement $context ){
		$links = $this->builder->buildIterator();

		foreach ( $this->xpath->query( './bp:links/bp:link' , $context ) as $link ){
			$links->append( $this->builder->buildLink( array(
				'type'	=> $this->getAttributeValue( $link , 'type' ),
				'url'	=> $this->getAttributeValue( $link , 'url' )
			) ) );
		}

		return $links;
	}

	/**
	 * @brief	Interpreta o nó offer que contém informações sobre ofertas de produtos
	 * @param	$result DOMElement O nó result do XML
	 * @param	$product BuscapeProduct O produto que contém a oferta
	 */
	private function parseOffer( DOMElement $result , BuscapeProduct $product ){
		foreach ( $this->xpath->query( './bp:offer' , $result ) as $offer ){
			$id = $this->getAttributeValue( $offer , 'id' , 'int' );
			$categoryId = $this->getAttributeValue( $offer , 'categoryId' , 'int' ); //FIXME: Deveríamos ter um ID do produto da oferta, não uma categoria
			$offerName = $this->getNodeValue( $offer , 'offerName' );
			$links = $this->parseLinks( $offer );
			$price	= $offer->getElementsByTagName( 'price' )->item( 0 );
			$priceValue = $this->getNodeValue( $price , 'value' , 'float' );
			$buscapeOffer =  $this->builder->buildOffer( array(
				'id'			=> $id,
				'categoryId'	=> $categoryId,
				'offerName'		=> $offerName,
				'links'			=> $links,
				'thumbnail'		=> $this->builder->buildThumbnail( array(
					'url' => $this->getAttributeValue( $offer->getElementsByTagName( 'thumbnail' )->item( 0 ) , 'url' )
				) ),
				'price'			=> $priceValue,
				'parcel'		=> $this->parseParcel( $price ),
				'seller'		=> $this->parseSellers( $offer )
			) );

			$product->addOffer( $buscapeOffer );
		}
	}

	/**
	 * @brief	Interpreta o nó contacts de um vendedor
	 * @param	$context DOMElement O nó seller que contém os contatos
	 * @return	RecursiveComponentIterator Lista de todos os contatos do vendedor
	 */
	private function parseContacts( DOMElement $context ){
		$contacts = $this->builder->buildIterator();

		foreach ( $this->xpath->query( './bp:contacts/bp:contact' , $context ) as $contact ){
			$label = $this->getAttributeValue( $contact , 'label' );
			$value = $this->getAttributeValue( $contact , 'value' );

			$buscapeContact = $this->builder->buildContact( array(
				'label'	=> $label,
				'value'	=> $value
			) );

			$contacts->append( $buscapeContact );
		}

		return $contacts;
	}

	/**
	 * @brief	Interpreta o nó parcel que contém informações sobre parcelamento de uma oferta
	 * @param	$context DOMElement O nó price da oferta
	 * @return	BuscapeParcel
	 */
	private function parseParcel( DOMElement $context ){
		$parcel = $this->xpath->query( './bp:parcel' , $context )->item( 0 );
		$value = $this->getNodeValue( $context , 'value' , 'float' );
		$number = $this->getNodeValue( $context , 'number' , 'int' );
		$interest = $this->getNodeValue( $context , 'interest' , 'float' );

		return $this->builder->buildParcel( array(
			'value'		=> $value,
			'number'	=> $number == 0 ? 1 : $number,
			'interest'	=> $interest
		) );
	}

	/**
	 * @brief	Interpreta o nó seller que contém informações sobre um vendedor
	 * @param	$context DOMElement O nó que contém o vendedor (offer)
	 * @return	RecursiveComponentIterator Lista de vendedores
	 */
	private function parseSellers( DOMElement $context ){
		$sellers = $this->builder->buildIterator();

		foreach ( $this->xpath->query( './bp:seller' , $context ) as $seller ){
			$digitalPayment = $this->getAttributeValue( $seller , 'pagamentoDigital' , 'bool' ); //FIXME: esse atributo deve ser traduzido no retorno
			$isTrustedStore = $this->getAttributeValue( $seller , 'isTrustedStore' , 'bool' );
			$id = $this->getAttributeValue( $seller , 'id' , 'int' );
			$sellerName = $this->getNodeValue( $seller , 'sellerName' );
			$cpc = $this->getNodeValue( $seller , 'cpc' , 'float' );
			$links = $this->parseLinks( $seller );
			$extra = $this->getNodeValue( $seller , 'extra' );
			$buscapeSeller = $this->builder->buildSeller( array(
				'digitalPayment'	=> $digitalPayment,
				'isTrustedStore'	=> $isTrustedStore,
				'id'				=> $id,
				'sellerName'		=> $sellerName,
				'cpc'				=> $cpc,
				'thumbnail'			=> $this->builder->buildThumbnail( array(
					'url' => $this->getAttributeValue( $seller->getElementsByTagName( 'thumbnail' )->item( 0 ) , 'url' )
				) ),
				'links'				=> $links,
				'extra'				=> $extra,
				'rating'			=> $this->parseRating( $seller ),
				'contacts'			=> $this->parseContacts( $seller ),
				'addresses'			=> $this->parseAddresses( $seller )
			) );

			$sellers->append( $buscapeSeller );
		}

		return $sellers;
	}

	/**
	 * @brief	Interpreta as categorias do XML
	 * @param	$result DOMElement Nó Result do XML
	 */
	private function parseProducts( DOMElement $result ){
		foreach ( $this->xpath->query( './/bp:product' ) as $product ){
			$id = $this->getAttributeValue( $product , 'id' , 'int' );
			$categoryId = $this->getAttributeValue( $product , 'categoryId' , 'int' );
			$totalSellers = $this->getAttributeValue( $product , 'totalSellers' , 'int' );
			$fullDescription = $this->getAttributeValue( $product , 'fullDescription' , 'bool' );
			$productName = $this->getNodeValue( $product , 'productName' );
			$productShortName = $this->getNodeValue( $product , 'productShortName' );
			$priceMin = $this->getNodeValue( $product , 'priceMin' , 'float' );
			$priceMax = $this->getNodeValue( $product , 'priceMax' , 'float' );
			$buscapeProduct = $this->builder->buildProduct( array(
				'id'				=> $id,
				'categoryId'		=> $categoryId,
				'totalSellers'		=> $totalSellers,
				'fullDescription'	=> $fullDescription,
				'productName'		=> $productName,
				'productShortName'	=> $productShortName,
				'priceMin'			=> $priceMin,
				'priceMax'			=> $priceMax,
				'thumbnail'			=> $this->builder->buildThumbnail( array(
					'url' => $this->getAttributeValue( $product->getElementsByTagName( 'thumbnail' )->item( 0 ) , 'url' )
				) ),
				'links'				=> $this->parseLinks( $product ),
				'specification'		=> $this->parseSpecification( $product ),
				'rating'			=> $this->parseRating( $product )
			) );

			$this->parseOffer( $result , $buscapeProduct );
		}
	}

	/**
	 * @brief	Interpreta o nó rating
	 * @param	$context DOMElement
	 * @return	BuscapeRating
	 */
	private function parseRating( DOMElement $context ){
		$ratingNode = $this->xpath->query( './bp:rating' , $context )->item( 0 );
		$userAverageRating = $ratingNode->getElementsByTagName( 'userAverageRating' )->item( 0 );
		$numComments = $this->getNodeValue( $userAverageRating , 'numComments' , 'int' );
		$rating = $this->getNodeValue( $userAverageRating , 'rating' , 'float' );
		$links = $this->parseLinks( $userAverageRating );
		$specification = $this->parseSpecification( $userAverageRating , true );

		$eBitRating = $ratingNode->getelementsByTagName( 'eBitRating' )->item( 0 );

		if ( is_null( $eBitRating ) ){
			$buscapeEBitRating = array();
		} else {
			$buscapeEBitRating = array(
				'numComments'	=> $this->getNodeValue( $eBitRating , 'numComments' , 'int' ),
				'rating'		=> $this->getNodeValue( $eBitRating , 'rating' )
			);
		}

		$buscapeRating = $this->builder->buildRating( array(
			'numComments'	=> $numComments,
			'rating'		=> $rating,
			'links'			=> $links,
			'specification'	=> $specification,
			'eBitRating'	=> $this->builder->buildRating( $buscapeEBitRating )
		) );

		return $buscapeRating;
	}

	/**
	 * @brief	Interpreta a especificação de um produto
	 * @param	$context DOMElement Nó specification do XML
	 * @param	valuesAsFloat boolean Indica que os valores da especificação devem ser tratados como float
	 * @return	BuscapeSpecification
	 */
	private function parseSpecification( DOMElement $context , $valuesAsFloat = false ){
		$specification = $this->xpath->query( './bp:specification' , $context )->item( 0 );
		$buscapeSpecification = $this->builder->buildSpecification();

		if ( !is_null( $specification ) ){
			$links = $this->parseLinks( $specification );

			foreach ( $this->xpath->query( './bp:item' , $specification ) as $item ){
				$buscapeItem = $this->builder->buildItem( array( 'label' => $this->getAttributeValue( $item , 'label' ) ) );

				foreach ( $item->getElementsByTagName( 'value' ) as $value ){
					$buscapeItem->add( $this->builder->buildValue( array( 'value' => $valuesAsFloat ? (float) $value->nodeValue : $value->nodeValue ) ) );
				}

				$buscapeSpecification->add( $buscapeItem );
			}
		}

		return $buscapeSpecification;
	}
}